package org.rsbot.script.methods;

import org.rsbot.script.wrappers.RSTile;

import java.awt.*;

import java.util.List;
import java.util.ArrayList;

/**
 * Tile related operations.
 * @author Wade
 */
public class Tiles extends MethodProvider {
	Tiles(final MethodContext ctx) {
		super(ctx);
	}

	/**
	 * Enum for collision flags. WALL_* flags cover both allow range and block range flags.
	 *
	 * Use value to get the integer value of the flag
	 */
	public enum Flag {
		WALL_NW(0x1),
		WALL_N(0x2),
		WALL_NE(0x4),
		WALL_E(0x8),
		WALL_SE(0x10),
		WALL_S(0x20),
		WALL_SW(0x40),
		WALL_W(0x80),

		BLOCKED(0x100),
		WALL_BLOCK_NW(0x200),
		WALL_BLOCK_N(0x400),
		WALL_BLOCK_NE(0x800),
		WALL_BLOCK_E(0x1000),
		WALL_BLOCK_SE(0x2000),
		WALL_BLOCK_S(0x4000),
		WALL_BLOCK_SW(0x8000),
		WALL_BLOCK_W(0x10000),
		OBJECT_BLOCK(0x20000),
		DECORATION_BLOCK(0x40000),
		MAP_BLOCK(0x200000),

		WALL_ALLOW_RANGE_NW(0x400000),
		WALL_ALLOW_RANGE_N(0x800000),
		WALL_ALLOW_RANGE_NE(0x1000000),
		WALL_ALLOW_RANGE_E(0x2000000),
		WALL_ALLOW_RANGE_SE(0x4000000),
		WALL_ALLOW_RANGE_S(0x8000000),
		WALL_ALLOW_RANGE_SW(0x10000000),
		WALL_ALLOW_RANGE_W(0x20000000),
		OBJECT_ALLOW_RANGE(0x40000000),

		WATER(0x1280100),

		NULL(0);

		final int value;

		private Flag(int value){
			this.value = value;
		}

		public String toString() {
			return this.name();
		}

		public int value() {
			return value;
		}
	}


	/**
	 * Clicks a tile if it is on screen with given offsets in 3D space.
	 *
	 * @param tile   The <code>RSTile</code> to do the action at.
	 * @param xd     Distance from bottom left of the tile to bottom right. Ranges
	 *               from 0-1.
	 * @param yd     Distance from bottom left of the tile to top left. Ranges from
	 *               0-1.
	 * @param h      Height to click the <code>RSTile</code> at. Use 1 for tables,
	 *               0 by default.
	 * @param action The action to perform at the given <code>RSTile</code>.
	 * @return <tt>true</tt> if no exceptions were thrown; otherwise
	 *         <tt>false</tt>.
	 */
	public boolean interact(final RSTile tile, final double xd, final double yd, final int h, final String action) {
		return methods.tiles.interact(tile, xd, yd, h, action, null);
	}

	/**
	 * Clicks a tile if it is on screen with given offsets in 3D space.
	 *
	 * @param tile   The <code>RSTile</code> to do the action at.
	 * @param xd     Distance from bottom left of the tile to bottom right. Ranges
	 *               from 0-1.
	 * @param yd     Distance from bottom left of the tile to top left. Ranges from
	 *               0-1.
	 * @param h      Height to click the <code>RSTile</code> at. Use 1 for tables,
	 *               0 by default.
	 * @param action The action to perform at the given <code>RSTile</code>.
	 * @param option The option to perform at the given <code>RSTile</code>.
	 * @return <tt>true</tt> if no exceptions were thrown; otherwise
	 *         <tt>false</tt>.
	 */
	public boolean interact(final RSTile tile, final double xd, final double yd, final int h, final String action, final String option) {
		final Point location = methods.calc.tileToScreen(tile, xd, yd, h);
		if (location.x != -1 && location.y != -1) {
			methods.mouse.move(location, 3, 3);
			sleep(random(20, 100));
			return methods.menu.click(action, option);
		}
		return false;
	}

	/**
	 * Clicks a tile if it is on screen. It will left-click if the action is
	 * available as the default option, otherwise it will right-click and check
	 * for the action in the context methods.menu.
	 *
	 * @param tile   The RSTile that you want to click.
	 * @param action Action command to use click
	 * @return <tt>true</tt> if the tile was clicked; otherwise
	 *         <tt>false</tt>.
	 */
	public boolean interact(final RSTile tile, final String action) {
		return methods.tiles.interact(tile, action, null);
	}

	/**
	 * Clicks a tile if it is on screen. It will left-click if the action is
	 * available as the default menu action, otherwise it will right-click and check
	 * for the action in the context methods.menu.
	 *
	 * @param tile   The RSTile that you want to click.
	 * @param action Action of the menu entry to click
	 * @param option Option of the menu entry to click
	 * @return <tt>true</tt> if the tile was clicked; otherwise
	 *         <tt>false</tt>.
	 */
	public boolean interact(final RSTile tile, final String action, final String option) {
		final Point point = methods.calc.tileToScreen(tile);
		if (point == null) {
			return false;
		}
		if ((point.x | point.y) == -1) {
			return false;
		}
		try {
			for (int i = 0; i++ < 5; ) {
				final Point location = methods.calc.tileToScreen(tile);
				if (location.x == -1 || location.y == -1) {
					return false;
				}
				methods.mouse.move(location, 5, 5);
				if (methods.menu.click(action, option)) {
					return true;
				}
			}
		} catch (Exception ignored) {	}
		return false;
	}


	/**
	 * Returns the RSTile under the mouse.
	 *
	 * @return The <code>RSTile</code> under the mouse, or null if the mouse is
	 *         not over the viewport.
	 */
	public RSTile getTileUnderMouse() {
		final Point p = methods.mouse.getLocation();
		if (!methods.calc.pointOnScreen(p)) {
			return null;
		}
		return getTileUnderPoint(p);
	}

	/**
	 * Gets the tile under a point.
	 *
	 * @param p The point.
	 * @return RSTile at the point's location
	 */
	public RSTile getTileUnderPoint(final Point p) {
		if (!methods.calc.pointOnScreen(p)) {
			return null;
		}
		RSTile close = null;
		for (int x = 0; x < 104; x++) {
			for (int y = 0; y < 104; y++) {
				final RSTile t = new RSTile(x + methods.client.getBaseX(), y + methods.client.getBaseY());
				final Point s = methods.calc.tileToScreen(t);
				if (s.x != -1 && s.y != -1) {
					if (close == null) {
						close = t;
					}
					if (methods.calc.tileToScreen(close).distance(p) > methods.calc.tileToScreen(t).distance(p)) {
						close = t;
					}
				}
			}
		}
		return close;
	}


	/**
	 * Draws a tile on screen and minimap
	 *
	 * @param render The instance of <code>Graphics</code> to use
	 * @param color The <code>Color</code> to paint the tile
	 * @param tile The <code>RSTile</code> to paint
	 * @param tileHeight Height offset (normal to the ground) to return the <code>Point</code> at.
	 */
	public void drawTile(final Graphics render, final Color color, final RSTile tile, final int tileHeight) {
		if (methods.calc.tileOnMap(tile)) {
			final Point p = methods.calc.tileToMinimap(tile);
			render.setColor(color);
			render.drawLine(p.x, p.y, p.x, p.y);
			if (methods.calc.tileOnScreen(tile)) {
				Point localPoint1 = methods.calc.tileToScreen(tile, 0.0D, 0.0D, tileHeight);
				Point localPoint2 = methods.calc.tileToScreen(tile, 1.0D, 0.0D, tileHeight);
				Point localPoint3 = methods.calc.tileToScreen(tile, 0.0D, 1.0D, tileHeight);
				Point localPoint4 = methods.calc.tileToScreen(tile, 1.0D, 1.0D, tileHeight);
				if (methods.calc.pointOnScreen(localPoint1) && methods.calc.pointOnScreen(localPoint2) &&
						methods.calc.pointOnScreen(localPoint3) && methods.calc.pointOnScreen(localPoint4)) {
					final Polygon localPolygon = new Polygon();
					localPolygon.addPoint(localPoint1.x, localPoint1.y);
					localPolygon.addPoint(localPoint2.x, localPoint2.y);
					localPolygon.addPoint(localPoint4.x, localPoint4.y);
					localPolygon.addPoint(localPoint3.x, localPoint3.y);
					render.drawPolygon(localPolygon);
					render.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 50));
					render.fillPolygon(localPolygon);
				}
			}
		}
	}

	/**
	 * Gets collision flags at a tile
	 *
	 * @param tile The <code>RSTile</code> to check
	 * @return A <code>Flag</code> array
	 */
	public Flag[] getFlagsAtTile(final RSTile tile) {
		final List<Flag> flagsArrayList = new ArrayList<Flag>();
		final int tileFlag = getRawFlagAtTile(tile);
		for(Flag flag : Flag.values()) {
			if((flag.value() & tileFlag) != 0) {
				flagsArrayList.add(flag);
			}
		}
		return flagsArrayList.toArray(new Flag[flagsArrayList.size()]);
	}

	/**
	 * Gets the raw collision flag for a tile
	 * May return when a new area of the map is loading	 *
	 *
	 * @param tile The <code>RSTile</code> to check
	 * @return An integer that represents the tile's collision flag
	 */
	public int getRawFlagAtTile(final RSTile tile) {
		try {
			final RSTile offset = methods.walking.getCollisionOffset(methods.game.getPlane());
			final int[][] flags = methods.walking.getCollisionFlags(methods.game.getPlane());
			return flags[tile.getX() - (methods.game.getBaseX() + offset.getX())]
				   [tile.getY() - (methods.game.getBaseY() + offset.getY())];
		} catch(ArrayIndexOutOfBoundsException e) {
			//New map area is loading
			//Might want to throw exception and let user handle?
			sleep(2000);
			return getRawFlagAtTile(tile);
		}
	}

	/**
	 * Checks to see if a tile is walkable
	 *
	 * @param tile The <code>RSTile</code> to check
	 * @return True if walkable
	 */
	public boolean isWalkable(final RSTile tile) {
		return (getRawFlagAtTile(tile) & (Flag.BLOCKED.value() | Flag.WATER.value())) == 0;
	}

	public boolean isSpecial(final RSTile tile) {
		final int tileFlag = getRawFlagAtTile(tile);
		return (tileFlag & Flag.BLOCKED.value()) == 0 && (tileFlag & Flag.WATER.value()) != 0;
	}


	/**
	 * Checks if the tile "t" is closer to the player than the tile "tt"
	 *
	 * @param t  First tile.
	 * @param tt Second tile.
	 * @return True if the first tile is closer to the player than the second
	 *         tile, otherwise false.
	 */
	public boolean isCloser(final RSTile t, final RSTile tt) {
		return methods.calc.distanceTo(t) < methods.calc.distanceTo(tt);
	}

	/**
	 * Creates a new <code>RSTile</code> relative to another
	 *
	 * @param tile The <code>RSTile</code> to which the new one is relative to
	 * @param xDeviation Number of tiles away on the X axis, can be negative or positive
	 * @param yDeviation Number of tiles away on the Y axis, can be negative or positive
	 * @return A new <code>RSTile</code>
	 */
	public RSTile deriveTile(final RSTile tile, final int xDeviation, final int yDeviation) {
		return new RSTile(tile.getX() + xDeviation, tile.getY() + yDeviation, tile.getZ());
	}

	public boolean doHover(final RSTile tile) {
		final Point p = methods.calc.tileToScreen(tile);
		if (p.getX() != -1) {
			methods.mouse.move(p);
			return true;
		}
		return false;
	}
}
